/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.records.merge;

import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.context.AbstractRecordIdentityContext;
import org.unidata.mdm.data.context.GetRecordTimelineRequestContext;
import org.unidata.mdm.data.context.PreviewRequestContext;
import org.unidata.mdm.data.context.RecordIdentityContextSupport;
import org.unidata.mdm.data.dto.GetRecordDTO;
import org.unidata.mdm.data.exception.DataExceptionIds;
import org.unidata.mdm.data.exception.DataProcessingException;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.impl.CommonRecordsComponent;
import org.unidata.mdm.data.type.data.OriginRecord;
import org.unidata.mdm.data.type.keys.RecordKeys;
import org.unidata.mdm.system.type.pipeline.Start;

/**
 * @author Mikhail Mikhailov
 * Simple context validity checker and key finder.
 */
@Component(RecordPreviewStartExecutor.SEGMENT_ID)
public class RecordPreviewStartExecutor extends Start<PreviewRequestContext, GetRecordDTO> implements RecordIdentityContextSupport {
    /**
     * Common component.
     */
    @Autowired
    private CommonRecordsComponent commonRecordsComponent;
    /**
     * This logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(RecordPreviewStartExecutor.class);
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RECORD_PREVIEW_START]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".record.preview.start.description";
    /**
     * Constructor.
     */
    public RecordPreviewStartExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION, PreviewRequestContext.class, GetRecordDTO.class);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void start(PreviewRequestContext ctx) {
        setup(ctx);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String subject(PreviewRequestContext ctx) {
        setup(ctx);
        RecordKeys keys = ctx.keys();
        return keys.getEntityName();
    }

    protected void setup(PreviewRequestContext ctx) {

        if (ctx.setUp()) {
            return;
        }

        setupTimelines(ctx);

        setupFields(ctx);

        ctx.setUp(true);
    }

    private void setupFields(PreviewRequestContext ctx) {
        ctx.timestamp(new Date());
    }

    /**
     * Both - validate and loading the timeline
     * @param ctx
     * @return
     */
    private void setupTimelines(PreviewRequestContext ctx) {

        if (ctx.getDuplicates().size() < (ctx.isValidRecordKey() ? 1 : 2)) {
            final String message = "Invalid input length - too few identities [{}] supplied for preview.";
            LOGGER.warn(message, ctx.getDuplicates().size());
            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_PREVIEW_INVALID_INPUT, ctx.getDuplicates().size());
        }

        AbstractRecordIdentityContext master = null;
        if (ctx.isValidRecordKey()) {
            master = ctx;
        } else {
            master = ctx.getDuplicates().listIterator().next();
            ctx.getDuplicates().listIterator().remove();
        }

        Timeline<OriginRecord> timeline = setupAndCheckTimeline(ctx, master);

        ctx.currentTimeline(timeline);
        ctx.keys(timeline.getKeys());

        Map<String, Timeline<OriginRecord>> duplicateTimelines = new HashMap<>();
        for (AbstractRecordIdentityContext dCtx : ctx.getDuplicates()) {
            Timeline<OriginRecord> dt = setupAndCheckTimeline(ctx, dCtx);
            duplicateTimelines.put(dt.getKeys().getEtalonKey().getId(), dt);
        }

        // Check master supplied twice
        if (duplicateTimelines.containsKey(timeline.getKeys().getEtalonKey().getId())) {
            final String message = "The key [{}] supplied twice - as master and duplicate.";
            LOGGER.warn(message, timeline.getKeys().getEtalonKey().getId());
            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_RECORD_PREVIEW_MASTER_ID_IN_DUPLICATES,
                    timeline.getKeys().getEtalonKey().getId());
        }

        ctx.discardedKeys(Collections.emptyList());
        ctx.duplicateKeys(duplicateTimelines.values().stream().map(Timeline::<RecordKeys>getKeys).collect(Collectors.toList()));
        ctx.duplicateTimelines(duplicateTimelines);
    }

    private Timeline<OriginRecord> setupAndCheckTimeline(PreviewRequestContext pCtx, AbstractRecordIdentityContext ctx) {

        // 1. Load interval view.
        Timeline<OriginRecord> timeline = commonRecordsComponent.loadTimeline(
                GetRecordTimelineRequestContext.builder(ctx)
                    .fetchData(true)
                    .forDate(pCtx.getForDate() == null ? new Date(System.currentTimeMillis()) : pCtx.getForDate())
                    .forLastUpdate(pCtx.getForLastUpdate())
                    .forOperationId(pCtx.getForOperationId())
                    .forUpdatesAfter(pCtx.getUpdatesAfter())
                    .build());

        RecordKeys keys = timeline.getKeys();
        if (keys == null) {
            final String message = "Record not found by supplied keys etalon id: [{}], origin id [{}], external id [{}], source system [{}], name [{}]";
            LOGGER.warn(message, ctx.getEtalonKey(), ctx.getOriginKey(), ctx.getExternalId(), ctx.getSourceSystem(), ctx.getEntityName());
            throw new DataProcessingException(message, DataExceptionIds.EX_DATA_PREVIEW_NOT_FOUND_BY_SUPPLIED_KEYS,
                    ctx.getEtalonKey(), ctx.getOriginKey(), ctx.getExternalId(), ctx.getSourceSystem(), ctx.getEntityName());
        }

        return timeline;
    }
}
